/*!
 * This file is part of Aloha Editor
 * Author & Copyright (c) 2010 Gentics Software GmbH, aloha@gentics.com
 * Licensed unter the terms of http://www.aloha-editor.com/license.html
 */
(function(window, undefined) {
	
	var
		jQuery = window.alohaQuery || window.jQuery, $ = jQuery,
//		GENTICS = window.GENTICS,
//		Aloha = window.Aloha,
		DOMUtils, TextRangeUtils, selection, DOMRange, RangeIterator, DOMSelection;

/*
 * Only execute the following code if we are in IE (check for
 * document.attachEvent, this is a microsoft event and therefore only available
 * in IE).
 */

if(document.attachEvent && document.selection) {
/*!
*   DOM Ranges for Internet Explorer (m2)
*
*   Copyright (c) 2009 Tim Cameron Ryan
*   Released under the MIT/X License
*   available at http://code.google.com/p/ierange/
*/

	/*
	  Range reference:
	    http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
	    http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsRange.cpp
	    https://developer.mozilla.org/En/DOM:Range
	  Selection reference:
	    http://trac.webkit.org/browser/trunk/WebCore/page/DOMSelection.cpp
	  TextRange reference:
	    http://msdn.microsoft.com/en-us/library/ms535872.aspx
	  Other links:
	    http://jorgenhorstink.nl/test/javascript/range/range.js
	    http://jorgenhorstink.nl/2006/07/05/dom-range-implementation-in-ecmascript-completed/
	    http://dylanschiemann.com/articles/dom2Range/dom2RangeExamples.html
	*/

	//[TODO] better exception support


	/*
	  DOM functions
	 */

	DOMUtils = {
		findChildPosition: function (node) {
			for (var i = 0; node = node.previousSibling; i++)
				continue;
			return i;
		},
		isDataNode: function (node) {
			return node && node.nodeValue !== null && node.data !== null;
		},
		isAncestorOf: function (parent, node) {
			return !DOMUtils.isDataNode(parent) &&
			    (parent.contains(DOMUtils.isDataNode(node) ? node.parentNode : node) ||
			    node.parentNode == parent);
		},
		isAncestorOrSelf: function (root, node) {
			return DOMUtils.isAncestorOf(root, node) || root == node;
		},
		findClosestAncestor: function (root, node) {
			if (DOMUtils.isAncestorOf(root, node))
				while (node && node.parentNode != root)
					node = node.parentNode;
			return node;
		},
		getNodeLength: function (node) {
			return DOMUtils.isDataNode(node) ? node.length : node.childNodes.length;
		},
		splitDataNode: function (node, offset) {
			if (!DOMUtils.isDataNode(node))
				return false;
			var newNode = node.cloneNode(false);
			node.deleteData(offset, node.length);
			newNode.deleteData(0, offset);
			node.parentNode.insertBefore(newNode, node.nextSibling);
		}
	};

	/*
	  Text Range utilities
	  functions to simplify text range manipulation in ie
	 */

	TextRangeUtils = {
		convertToDOMRange: function (textRange, document) {
			var domRange,adoptBoundary;

			adoptBoundary = function(domRange, textRange, bStart) {
				// iterate backwards through parent element to find anchor location
				var cursorNode = document.createElement('a'),
					cursor = textRange.duplicate(),
					parent;
			
				cursor.collapse(bStart);
				parent = cursor.parentElement();
				do {
					parent.insertBefore(cursorNode, cursorNode.previousSibling);
					cursor.moveToElementText(cursorNode);
				} while (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) > 0 && cursorNode.previousSibling);

				// when we exceed or meet the cursor, we've found the node
				if (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) == -1 && cursorNode.nextSibling) {
					// data node
					cursor.setEndPoint(bStart ? 'EndToStart' : 'EndToEnd', textRange);
					domRange[bStart ? 'setStart' : 'setEnd'](cursorNode.nextSibling, cursor.text.length);
				} else {
					// element
					domRange[bStart ? 'setStartBefore' : 'setEndBefore'](cursorNode);
				}
				cursorNode.parentNode.removeChild(cursorNode);
			};

			// return a DOM range
			domRange = new DOMRange(document);
			adoptBoundary(domRange, textRange, true);
			adoptBoundary(domRange, textRange, false);
			return domRange;
		},

		convertFromDOMRange: function (domRange) {
			function adoptEndPoint(textRange, domRange, bStart) {
				// find anchor node and offset
				var container = domRange[bStart ? 'startContainer' : 'endContainer'],
					offset = domRange[bStart ? 'startOffset' : 'endOffset'], textOffset = 0,
					anchorNode = DOMUtils.isDataNode(container) ? container : container.childNodes[offset],
					anchorParent = DOMUtils.isDataNode(container) ? container.parentNode : container,
					cursorNode, cursor;
				
				// visible data nodes need a text offset
				if (container.nodeType == 3 || container.nodeType == 4) {
					textOffset = offset;
				}

				// create a cursor element node to position range (since we can't select text nodes)
				cursorNode = domRange._document.createElement('a');
				anchorParent.insertBefore(cursorNode, anchorNode);
				cursor = domRange._document.body.createTextRange();
				cursor.moveToElementText(cursorNode);
				cursorNode.parentNode.removeChild(cursorNode);
				// move range
				textRange.setEndPoint(bStart ? 'StartToStart' : 'EndToStart', cursor);
				textRange[bStart ? 'moveStart' : 'moveEnd']('character', textOffset);
			}

			// return an IE text range
			var textRange = domRange._document.body.createTextRange();
			adoptEndPoint(textRange, domRange, true);
			adoptEndPoint(textRange, domRange, false);
			return textRange;
		}
	};

	/*
	  DOM Range
	 */
	DOMRange = function(document) {
		// save document parameter
		this._document = document;

		// initialize range
		//[TODO] this should be located at document[0], document[0]
		this.startContainer = this.endContainer = document.body;
		this.endOffset = DOMUtils.getNodeLength(document.body);
	};

	DOMRange.START_TO_START = 0;
	DOMRange.START_TO_END = 1;
	DOMRange.END_TO_END = 2;
	DOMRange.END_TO_START = 3;

	DOMRange.prototype = {
		// public properties
		startContainer: null,
		startOffset: 0,
		endContainer: null,
		endOffset: 0,
		commonAncestorContainer: null,
		collapsed: false,
		// private properties
		_document: null,

		// private methods
		_refreshProperties: function () {
			// collapsed attribute
			this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset);
			// find common ancestor
			var node = this.startContainer;
			while (node && node != this.endContainer && !DOMUtils.isAncestorOf(node, this.endContainer))
				node = node.parentNode;
			this.commonAncestorContainer = node;
		},

		// range methods
	//[TODO] collapse if start is after end, end is before start
		setStart: function(container, offset) {
			this.startContainer = container;
			this.startOffset = offset;
			this._refreshProperties();
		},
		setEnd: function(container, offset) {
			this.endContainer = container;
			this.endOffset = offset;
			this._refreshProperties();
		},
		setStartBefore: function (refNode) {
			// set start to beore this node
			this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode));
		},
		setStartAfter: function (refNode) {
			// select next sibling
			this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
		},
		setEndBefore: function (refNode) {
			// set end to beore this node
			this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode));
		},
		setEndAfter: function (refNode) {
			// select next sibling
			this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
		},
		selectNode: function (refNode) {
			this.setStartBefore(refNode);
			this.setEndAfter(refNode);
		},
		selectNodeContents: function (refNode) {
			this.setStart(refNode, 0);
			this.setEnd(refNode, DOMUtils.getNodeLength(refNode));
		},
		collapse: function (toStart) {
			if (toStart)
				this.setEnd(this.startContainer, this.startOffset);
			else
				this.setStart(this.endContainer, this.endOffset);
		},

		// editing methods
		cloneContents: function () {
			// clone subtree
			return (function cloneSubtree(iterator) {
				for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) {
					node = node.cloneNode(!iterator.hasPartialSubtree());
					if (iterator.hasPartialSubtree())
						node.appendChild(cloneSubtree(iterator.getSubtreeIterator()));
					frag.appendChild(node);
				}
				return frag;
			})(new RangeIterator(this));
		},
		extractContents: function () {
			// cache range and move anchor points
			var range = this.cloneRange();
			if (this.startContainer != this.commonAncestorContainer)
				this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
			this.collapse(true);
			// extract range
			return (function extractSubtree(iterator) {
				for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) {
					if ( iterator.hasPartialSubtree() ) {
						node = node.cloneNode(false);
					}
					else {
						iterator.remove();
					}
					if (iterator.hasPartialSubtree())
						node.appendChild(extractSubtree(iterator.getSubtreeIterator()));
					frag.appendChild(node);
				}
				return frag;
			})(new RangeIterator(range));
		},
		deleteContents: function () {
			// cache range and move anchor points
			var range = this.cloneRange();
			if (this.startContainer != this.commonAncestorContainer)
				this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
			this.collapse(true);
			// delete range
			(function deleteSubtree(iterator) {
				while (iterator.next()) {
					if ( iterator.hasPartialSubtree() ) {
						deleteSubtree(iterator.getSubtreeIterator());
					}
					else {
						iterator.remove();
					}
				}
			})(new RangeIterator(range));
		},
		insertNode: function (newNode) {
			// set original anchor and insert node
			if (DOMUtils.isDataNode(this.startContainer)) {
				DOMUtils.splitDataNode(this.startContainer, this.startOffset);
				this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling);
			} else {
				this.startContainer.insertBefore(newNode, this.startContainer.childNodes[this.startOffset]);
			}
			// resync start anchor
			this.setStart(this.startContainer, this.startOffset);
		},
		surroundContents: function (newNode) {
			// extract and surround contents
			var content = this.extractContents();
			this.insertNode(newNode);
			newNode.appendChild(content);
			this.selectNode(newNode);
		},

		// other methods
		compareBoundaryPoints: function (how, sourceRange) {
			// get anchors
			var containerA, offsetA, containerB, offsetB;
			switch (how) {
			    case DOMRange.START_TO_START:
			    case DOMRange.START_TO_END:
				containerA = this.startContainer;
				offsetA = this.startOffset;
				break;
			    case DOMRange.END_TO_END:
			    case DOMRange.END_TO_START:
				containerA = this.endContainer;
				offsetA = this.endOffset;
				break;
			}
			switch (how) {
			    case DOMRange.START_TO_START:
			    case DOMRange.END_TO_START:
				containerB = sourceRange.startContainer;
				offsetB = sourceRange.startOffset;
				break;
			    case DOMRange.START_TO_END:
			    case DOMRange.END_TO_END:
				containerB = sourceRange.endContainer;
				offsetB = sourceRange.endOffset;
				break;
			}

			// compare
			return containerA.sourceIndex < containerB.sourceIndex ? -1 :
			    containerA.sourceIndex == containerB.sourceIndex ?
			        offsetA < offsetB ? -1 : offsetA == offsetB ? 0 : 1
			        : 1;
		},
		cloneRange: function () {
			// return cloned range
			var range = new DOMRange(this._document);
			range.setStart(this.startContainer, this.startOffset);
			range.setEnd(this.endContainer, this.endOffset);
			return range;
		},
		detach: function () {
	//[TODO] Releases Range from use to improve performance.
		},
		toString: function () {
			return TextRangeUtils.convertFromDOMRange(this).text;
		},
		createContextualFragment: function (tagString) {
			// parse the tag string in a context node
			var
				content = (DOMUtils.isDataNode(this.startContainer) ? this.startContainer.parentNode : this.startContainer).cloneNode(false),
				fragment;
			
			content.innerHTML = tagString;
			// return a document fragment from the created node
			for (fragment = this._document.createDocumentFragment(); content.firstChild; )
				fragment.appendChild(content.firstChild);
			return fragment;
		}
	};

	/*
	  Range iterator
	 */
	RangeIterator = function(range) {
		this.range = range;
		if (range.collapsed) {
			return;
		}

		//[TODO] ensure this works
		// get anchors
		var root = range.commonAncestorContainer;
		this._next = range.startContainer == root && !DOMUtils.isDataNode(range.startContainer) ?
		range.startContainer.childNodes[range.startOffset] :
		DOMUtils.findClosestAncestor(root, range.startContainer);
		this._end = range.endContainer == root && !DOMUtils.isDataNode(range.endContainer) ?
		range.endContainer.childNodes[range.endOffset] :
		DOMUtils.findClosestAncestor(root, range.endContainer).nextSibling;
	};

	RangeIterator.prototype = {
		// public properties
		range: null,
		// private properties
		_current: null,
		_next: null,
		_end: null,

		// public methods
		hasNext: function () {
			return !!this._next;
		},
		next: function () {
			// move to next node
			var current = this._current = this._next;
			this._next = this._current && this._current.nextSibling != this._end ?
			    this._current.nextSibling : null;

			// check for partial text nodes
			if (DOMUtils.isDataNode(this._current)) {
				if (this.range.endContainer == this._current)
					(current = current.cloneNode(true)).deleteData(this.range.endOffset, current.length - this.range.endOffset);
				if (this.range.startContainer == this._current)
					(current = current.cloneNode(true)).deleteData(0, this.range.startOffset);
			}
			return current;
		},
		remove: function () {
			var end, start;
			// check for partial text nodes
			if (DOMUtils.isDataNode(this._current) &&
			    (this.range.startContainer == this._current || this.range.endContainer == this._current)) {
				start = this.range.startContainer == this._current ? this.range.startOffset : 0;
				end = this.range.endContainer == this._current ? this.range.endOffset : this._current.length;
				this._current.deleteData(start, end - start);
			} else
				this._current.parentNode.removeChild(this._current);
		},
		hasPartialSubtree: function () {
			// check if this node be partially selected
			return !DOMUtils.isDataNode(this._current) &&
			    (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer) ||
			        DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer));
		},
		getSubtreeIterator: function () {
			// create a new range
			var subRange = new DOMRange(this.range._document);
			subRange.selectNodeContents(this._current);
			// handle anchor points
			if (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer))
				subRange.setStart(this.range.startContainer, this.range.startOffset);
			if (DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer))
				subRange.setEnd(this.range.endContainer, this.range.endOffset);
			// return iterator
			return new RangeIterator(subRange);
		}
	};

	/*
	  DOM Selection
	 */

	//[NOTE] This is a very shallow implementation of the Selection object, based on Webkit's
	// implementation and without redundant features. Complete selection manipulation is still
	// possible with just removeAllRanges/addRange/getRangeAt.

	DOMSelection = function (document) {
		// save document parameter
		this._document = document;

		// add DOM selection handler
		var selection = this;
		document.attachEvent('onselectionchange', function () { selection._selectionChangeHandler(); });
	};

	DOMSelection.prototype = {
		// public properties
		rangeCount: 0,
		// private properties
		_document: null,

		// private methods
		_selectionChangeHandler: function () {
			// check if there exists a range
			this.rangeCount = this._selectionExists(this._document.selection.createRange()) ? 1 : 0;
		},
		_selectionExists: function (textRange) {
			// checks if a created text range exists or is an editable cursor
			return textRange.compareEndPoints('StartToEnd', textRange) !== 0 ||
			    textRange.parentElement().isContentEditable;
		},

		// public methods
		addRange: function (range) {
			// add range or combine with existing range
			var selection = this._document.selection.createRange(), textRange = TextRangeUtils.convertFromDOMRange(range);
			if (!this._selectionExists(selection))
			{
				// select range
				textRange.select();
			}
			else
			{
				// only modify range if it intersects with current range
				if (textRange.compareEndPoints('StartToStart', selection) == -1)
					if (textRange.compareEndPoints('StartToEnd', selection) > -1 &&
					    textRange.compareEndPoints('EndToEnd', selection) == -1)
						selection.setEndPoint('StartToStart', textRange);
				else
					if (textRange.compareEndPoints('EndToStart', selection) < 1 &&
					    textRange.compareEndPoints('EndToEnd', selection) > -1)
						selection.setEndPoint('EndToEnd', textRange);
				selection.select();
			}
		},
		removeAllRanges: function () {
			// remove all ranges
			this._document.selection.empty();
		},
		getRangeAt: function (index) {
			// return any existing selection, or a cursor position in content editable mode
			var textRange = this._document.selection.createRange();
			if (this._selectionExists(textRange))
				return TextRangeUtils.convertToDOMRange(textRange, this._document);
			return null;
		},
		toString: function () {
			// get selection text
			return this._document.selection.createRange().text;
		}
	};

	/*
	  scripting hooks
	 */

	document.createRange = function () {
		return new DOMRange(document);
	};

	selection = new DOMSelection(document);
		window.getSelection = function () {
		return selection;
	};

	//[TODO] expose DOMRange/DOMSelection to window.?
}

})(window);